๐งจ1-2 ๊ณผ์  ํ๊ณ
์ํฐ๋ ํ๋ฆฌ์จ๋ณด๋ฉ ๋ ๋ฒ์งธ ๊ณผ์ ๋ github API๋ฅผ ์ด์ฉํด ๋ ๊ฐ์ง ํ์ด์ง (์ด์ ๋ชฉ๋ก๊ณผ ์์ธ ํ์ด์ง)๋ฅผ ๋ง๋๋ ๊ณผ์ ์๋ค. ์ธ๋ถ์ ์ธ ์๊ตฌ์ฌํญ์์ ๊ฐ์ฅ ์ค์ํ๋ ๋ถ๋ถ์ ๋ฆฌ์คํธ์์ Github API ๋ฐ์ดํฐ ์์ฒญ, context API๋ฅผ ํ์ฉํ API์ฐ๋, ๋ค์ฏ ๋ฒ์งธ ์ ์ ๊ด๊ณ ์ด๋ฏธ์ง๋ฅผ ๋ฃ์ด์ค ๊ฒ, ์คํฌ๋กค์ ๋ด๋ฆฌ๋ฉด ์ด์ ๋ชฉ๋ก์ ์ถ๊ฐ ๋ก๋ฉ์ด ๋ ์ ์๊ฒ infinity scroll์ ๊ตฌํํ ๊ฒ์ด์๋ค. ์ ์๊ตฌ์ฌํญ์ ์ด๋ป๊ฒ ํด๊ฒฐํ๋์ง์ ๋ํด ์ ๋ฆฌํ๊ณ ์ ๋ฆฌํด ๋ณด๊ณ ์ ํ๋ค.
1. Github Issue API
์ฒซ๋ฒ์งธ ๋ฌธ์ ๋ ๊ฐ๋จํ๊ฒ ํด๊ฒฐํ ์ ์์๋ค. ๋ฌธ์๋ฅผ ๋ณด๋ list issue API์์ ๊ธฐ๋ณธ์ ์ผ๋ก open๋๊ฑธ ๋ถ๋ฌ์ค๊ณ sort query๋ฅผ ์ด์ฉํด์ comment๊ฐ ๋ง์ ์์ผ๋ก ๋ฐ์์ฌ ์ ์์๋ค.
  
    
๋ฌธ์์์ ๋ฐ์ํ ์ ์๋ ์๋ฌ๋ 404์ 422 ๋ ๊ฐ์ง์ด๊ธฐ ๋๋ฌธ์ ๋ ๊ฐ์ง ์๋ฌ์ ๋ฐ๋ผ ์๋ฌ๋ฉ์์ง๋ฅผ ์ปค์คํ ํ ์ ์๊ฒ ์ด์ ์ ๋ง๋ค์ด๋ httpError class๋ฅผ ์ด์ฉํด ์๋ง์ ์๋ฌ๋ฅผ ๋ฐํํด ์ค ์ ์๊ฒ ํ๋ค.
//issueService.js
import HTTPError from '../network/httpError';
const getIssueList = async page => {
  const response = await fetch(
    `https://api.github.com/repos/angular/angular-cli/issues?sort=comments&per_page=30&page=${page}`,
    {
      method: 'GET',
      headers: {
        Authorization: `token ${process.env.REACT_APP_TOKEN}`,
      },
    }
  );
  if (!response.ok) {
    throw new HTTPError(response.status, response.statusText);
  } else {
    const data = await response.json();
    return data;
  }
};
export default getIssueList;
//httpError.js
export default class HTTPError extends Error {
  constructor(statusCode, message) {
    super(message);
    this.name = 'HTTPError';
    this.statusCode = statusCode;
  }
  get errorMessage() {
    switch (this.statusCode) {
      case 404:
        this.message = 'ํด๋น ๋ ํฌ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.';
        break;
      case 422:
        this.message = '์์ฒญ์ด ์๋ชป๋ endpoint๋ก ์ ๋ฌ๋์์ต๋๋ค';
        break;
      default:
        throw new Error('Unknown Error');
    }
    return this.message;
  }
}2. context API๋ฅผ ํ์ฉํ API ์ฐ๋
Context API
context API๋ ์ ์ญ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ผ๋ก, provider๋ด๋ถ์ ์ปดํฌ๋ํธ๋ค์๊ฒ ์ ๋ฌ์ prop์ผ๋ก ์์ ์ปดํฌ๋ํธ์ ํ๋ํ๋ ์ ๋ฌํ๋๊ฒ ์๋๋ผ, ํ์ํ ์ปดํฌ๋ํธ์์ ๋ฐ๋ก ์ํ์ ์ ๊ทผํ ์ ์๋ db์ ๊ฐ์ ์ญํ ์ ํ ์ ์๋ค. ์ด๋ฒ ๊ณผ์ ์์ context API๋ฅผ ์ฌ์ฉํ๋ ค๊ณ ํ๋ค๋ฉด issue list๋ฅผ ๋ถ๋ฌ์ค๊ณ ๋ถ๋ฌ์จ ๋ฐ์ดํฐ๋ฅผ context API์ ๋ฃ์ด์ฃผ์ด, list๋ด์ฉ์ด ํ์ํ ๊ณณ์์ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๊ฒ ๋ง๋ค์๋ค.

context API ์์ฒด์์ api๋ฅผ ์ด์ฉํด ๊ฐ์ ๋ฃ์ด๋๊น ์๊ฐ์ ํ์ง๋ง, ๋ด๋ถ์์ ๊ณ์ ๊ฐ์ด ๋ฐ๋๋ฉด ๋ฐ๋ ์์ ์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ฐ์ดํฐ๊ฐ ์ ๋ฌ๋ ์๋ ์์ ๊ฒ ๊ฐ์ ๋จ์์ด ๋ฐ์ดํฐ๋ง ๋ณด๊ดํ๊ณ ๋ณ๊ฒฝํ ์ ์๋ ํจ์๋ฅผ context๋ก ๊ฐ์ด ์ ๊ณตํ๋ ๋ฐฉ์์ผ๋ก ์ฝ๋๋ฅผ ๊ตฌ์ฑํ๋ค.
import { useMemo, useState, createContext } from "react"
export const ListContext = createContext()
export const ListContextProvider = ({ children }) => {
  const [issues, setIssues] = useState({})
  const setNextPage = () => setPage(page + 1)
  const value = useMemo(
    () => ({ issues, page, setNextPage, setIssues }),
    [issues, page]
  )
  return <ListContext.Provider value={value}>{children}</ListContext.Provider>
}Custom Hook: useFetch
๋๊ฐ์ง ํ์ด์ง ์ค ์ด๋๋ฅผ ๋จผ์  ์ ์ํด๋, api๋ก ๋ฐ์ดํฐ๋ฅผ contextAPI์ ์ ์ฅํ๊ฒ ํ๊ธฐ ์ํด์๋ ๋์ผํ ๋ก์ง์ ๋ ํ์ด์ง ๋ชจ๋ ๊ฐ์ง๊ณ ์์ด์ผ ํ๋ค. ๋ก์ง์ ์ฌ์ฌ์ฉ์ ์ํด useFetch๋ผ๋ custon Hook์ ๋ง๋ค์ด์ ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์๊ฒ ํ๊ณ , ๋ ํ์ด์ง ์ค ์ด๋๋ฅผ ์ ์ํด๋ ๋ฆฌ์คํธ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ์ด ๋ ์ข๊ฒ ๊ตฌ์ฑํ ์ ์๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค.
useEffect ๋ด๋ถ์์๋ async await์ผ๋ก ํจ์๋ฅผ ๊ฐ์ธ๋ฉด promise๊ฐ ๋ฐํ๋๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ ์ ์์ด, getList๋ฅผ ํจ์๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด์ useEffect๋ก ์คํํด ์ฃผ๋ ๋ก์ง์ ์ฌ์ฉํ๋ค.
const useFetch = () => {
  const { issues, setIssues, page } = useContext(ListContext)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState("")
  const getList = async () => {
    setIsLoading(true)
    try {
      const data = await getIssueList(page)
      if (data.length === 0) {
        setLastPage(true)
      }
      setIssues(prev => {
        const updated = { ...prev }
        data.forEach(issue => {
          updated[issue.id] = issue
        })
        return updated
      })
    } catch (error) {
      setError(error.errorMessage)
    }
    setIsLoading(false)
  }
  useEffect(() => {
    getList()
  }, [page])
  return [isLoading, error, issues, lastPage]
}issues ์ ์ญ์ํ ์๋ฃ๊ตฌ์กฐ
context ๋ด๋ถ ์ํ๋ฅผ ์ฒ์์๋ ๋ฐฐ์ด์ ์ด์ฉํด api ๋ฐ์ดํฐ๋ค์ ๋ด์๋๋ ค๊ณ ํ๋ค. ํ์ง๋ง ์์ธํ์ด์ง์ ๊ฐ๋ค๊ฐ ๋์์์ ๋, api๊ฐ ํธ์ถ๋๋ฉด์ ๊ฐ์ ๋ฐ์ดํฐ๊ฐ ๋๊ฐ์ฉ ๋ค์ด๊ฐ๋ ์ค๋ฅ๊ฐ ์๊ฒผ๋ค. ์ด๋ฌํ ์ค๋ฅ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ Set์ ์ด์ฉํด ์ค๋ณต๋ ๋ฐ์ดํฐ๋ ์ ๊ฑฐํ๋ ค ํ์ง๋ง, ์ฌ์ ํ ๋จ์์์๋ค. ์ค๋ณต์ ๊ฑฐ๊ฐ ๋์ง ์์๋ ์ด์ ๋ ๋ฐ์ดํฐ object๋ค์ด ๊ฐ์ ์๋ฃ๋ฅผ ๊ฐ์ง๊ณ ์์ง๋ง ๋ค๋ฅธ ์ฐธ์กฐ๊ฐ์ ๊ฐ์ง๊ณ ์์ด ๋ค๋ฅธ ๊ฐ์ผ๋ก ์ฒ๋ฆฌ๊ฐ ๋๋ค๊ณ ์๊ฐํ๋ค.
์ด๋ฌํ ์ค๋ณต์ ์ ๊ฑฐํ๊ธฐ ์ํด์ ์ฒ์์๋ ๋ฐ์ ๋ฐฐ์ด๊ณผ ๊ธฐ์กด ๋ฐฐ์ด์ ๋น๊ตํ๋ ๋ก์ง์ ์ง๋ ค๊ณ ํ์ง๋ง, O(n^2)์ ์๊ฐ๋ณต์ก๋๋ฅผ ๊ฐ์ง๊ธฐ ๋๋ฌธ์ ์๋ฃ์์ด ๋ง์์ง์๋ก ์ฑ๋ฅ์ด ์ ์ข์์ง ๊ฒ์ด๋ผ๋ ์๊ฐ์ด ๋ค์๋ค.
์ด์ ์ ํด๊ฒฐํ๊ธฐ ์ํด์ ์๋ฃ๊ตฌ์กฐ๋ฅผ Object๋ก ๋ฐ๊พธ์๋ค. object์ key๋ฅผ data์ id๋ก, value๋ฅผ ๋ฐ์ดํฐ ์์ฒด๋ก ํ ์ค๋ธ์ ํธ๋ฅผ ๋ง๋ค๋ฉด, ์ค๋ณต์ ๊ฐ๋จํ๊ฒ ์ ๊ฑฐํ ์ ์๊ณ ์ดํ์ ๋ฐฐ์ด๋ก ๋ฐ๊พธ์ด mappingํ ๋ ์ ๋ ฌ๋ง ํด์ฃผ๋ฉด ๋๊ธฐ ๋๋ฌธ์ O(nlogn)์ผ๋ก ๋ณด๋ค ๋์ ์ฑ๋ฅ์ ๊ฐ๊ฒ ๋ ๊ฒ์ด๋ผ ์์ํ๋ค.
const IssueList = () => {
  const { setNextPage } = useContext(ListContext)
  const [isLoading, error, issues, lastPage] = useFetch()
  return (
    <>
      <S.List>
        {Object.values(issues)
          .sort((a, b) => b.comments - a.comments)
          .map((issue, idx) => {
            return <IssueItem key={issue.id} {...issue} />
          })}
        {isLoading && <Loader />}
      </S.List>
    </>
  )
}
export default IssueList์๋ฃ๊ตฌ์กฐ๋ฅผ object๋ก ๋ฐ๊พผ ๋๋ถ์ detailํ์ด์ง์์ ๋ณด์ฌ์ค ๋๋ ๋ค๋ฅธ api ํธ์ถ์์ด useParam์ผ๋ก ๋ฐ์์จ id๊ฐ์ผ๋ก issues์ ์ ๊ทผํด ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์๋ค.
import React, { memo } from "react"
import { useNavigate, useParams } from "react-router"
import S from "./styles"
import formatDate from "../../utils/formatDate"
const IssueItem = ({ id, number, title, user, created_at, comments }) => {
  const navigate = useNavigate()
  const params = useParams()
  const date = formatDate(created_at)
  const handleClick = () => {
    if (!params.id) {
      navigate(`/detail/${id}`)
    }
  }
  return (
    <S.List onClick={handleClick} params={!!params.id}>
      <S.LeftBox>
        <header>
          <span>{`#${number}`}</span>
          <S.Title>{title}</S.Title>
        </header>
        <div>
          <span>{`์์ฑ์: ${user && user.login}`}</span>
          <span>{date}</span>
        </div>
      </S.LeftBox>
      <S.RightBox>
        ์ฝ๋ฉํธ:
        {comments}
      </S.RightBox>
    </S.List>
  )
}
export default memo(IssueItem)3. ๋ค์ฏ๋ฒ์งธ ์ ์ ๊ด๊ณ ๋ณด์ฌ์ฃผ๊ธฐ
list์ ํน์ ๋ถ๋ถ์ ์ถ๊ฐ๋ ๊ฒ์ ๋ณด์ฌ์ค ๊ฒ์ ํด๋ณธ ์ ์ด ์์ด์ ๊ณ ๋ฏผํ๋ค, mapping์ ํ ๋ index๊ฐ 4๊ฐ ๋์์ ๋ issueItem ์ปดํฌ๋ํธ์ ํจ๊ป adBox ์ปดํฌ๋ํธ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฐฉ์์ ์ ํํ๋ค. ํ์ง๋ง key๊ฐ ๊ณ์ํด์ ์ค๋ณต๋๋ค๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
import React, { useContext } from "react"
import S from "./styles"
import IssueItem from "../issueItem/IssueItem"
import AdBox from "../adBox/AdBox"
import useFetch from "../../hooks/useFetch"
import useObservation from "../../hooks/useObservation"
import { ListContext } from "../../context/ListContext"
import Loader from "../loader/Loader"
const IssueList = () => {
  const { setNextPage } = useContext(ListContext)
  const [isLoading, error, issues, lastPage] = useFetch()
  return (
    <>
      <S.List>
        {Object.values(issues)
          .sort((a, b) => b.comments - a.comments)
          .map((issue, idx) => {
            if (idx === 4) {
              return (
                <>
                  <AdBox />
                  <IssueItem key={issue.id} {...issue} />
                </>
              )
            }
            return <IssueItem key={issue.id} {...issue} />
          })}
        {isLoading && <Loader />}
      </S.List>
    </>
  )
}
export default IssueList
 
์ด๋์ ๊ณ์ํด์ ์๋ฌ๊ฐ ๋์ค๋์ง ์ฐพ๋ ์ค์ issueItem์ key๊ฐ์ ์ฃผ์๊ธฐ ๋๋ฌธ์ ์๋ฌ๊ฐ ๋ฐ์๋์๋ค๋ ๊ฒ์ ์๊ฒ๋์๋ค. ํด๊ฒฐํ๊ธฐ ์ํด์ fragment๊ฐ ์๋๋ผ div๋ก ๊ฐ์ธ์ฃผ๊ณ div์ key๊ฐ์ ์ ๋ฌํด์ค ์๋ฌ๋ฅผ ํด๊ฒฐํ ์ ์์๋ค.
import React, { useContext } from "react"
import S from "./styles"
import IssueItem from "../issueItem/IssueItem"
import AdBox from "../adBox/AdBox"
import useFetch from "../../hooks/useFetch"
import useObservation from "../../hooks/useObservation"
import { ListContext } from "../../context/ListContext"
import Loader from "../loader/Loader"
const IssueList = () => {
  const { setNextPage } = useContext(ListContext)
  const [isLoading, error, issues, lastPage] = useFetch()
  return (
    <>
      <S.List>
        {Object.values(issues)
          .sort((a, b) => b.comments - a.comments)
          .map((issue, idx) => {
            if (idx === 4) {
              return (
                <div key={issue.id}>
                  <AdBox />
                  <IssueItem {...issue} />
                </div>
              )
            }
            return <IssueItem key={issue.id} {...issue} />
          })}
        {isLoading && <Loader />}
      </S.List>
      {!lastPage ? (
        <S.Target ref={targetRef} />
      ) : (
        <S.Banner>๋ง์ง๋ง ํ์ด์ง์
๋๋ค๐</S.Banner>
      )}
    </>
  )
}
export default IssueList(๋์ค์ ์ ์ฌ์ค์ด์ง๋ง react.fragment์๋ key๊ฐ์ ์ค ์ ์๋ค๊ณ ํ๋ค.)
4. Infinite Scroll
์ด๋ฒ ๊ณผ์ ์์์ ๊ฐ์ฅ ํฐ ํต์ฌ ์กฐ๊ฑด์ด์๋ค. ํ ์ค๋์ ์คํ์ฑํ ๋ฐฉ์์๋ ๊ฐ๊ฐํ ์ฌ๋ผ์ค๋ infinite scroll์ ๋ํ ์ง๋ฌธ๋ค์ ๋ณด๋ฉด์ ์ ๊ฒ ์ ํ์ํ์ง๋ผ๋ ์๊ฐ์ ํ์๋๋ฐ ์ด๋ฒ์ ์ง์  ๊ตฌํํด๋ณด๋ฉด์ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํฌ ์ ์๋ ๊ธฐ๋ฅ์ด๋ผ๋ ์ ์ ๋ง์ด ๋๊ผ๋ค. ํ๋ฒ๋ ๊ตฌํํด๋ณธ ์ ์ด ์๋ ๊ธฐ๋ฅ์ด๊ธฐ ๋๋ฌธ์ ๊ด๋ จ ๊ธ์ ๋ง์ด ์ฐพ์๋ณด๊ณ ๊ฐ์ฅ ์ ์ ๋ฆฌ๋์ด ์๋ ์นด์นด์ค ์ํฐํ๋ผ์ด์ฆ์ ๊ธ์ ์ฐธ๊ณ ํด ๋ง๋ค์ด๋ณด์๋ค.
๋ง๋๋ ๋ฐฉ์์ scroll event๋ฅผ ์ด์ฉํ๋ ๋ฐฉ์๊ณผ Intersection Observer API๋ฅผ ์ฌ์ฉํ๋ ๋ ๊ฐ์ง ๋ฐฉ์์ด ์ค๋ช ๋์ด ์์๋๋ฐ, ์ด์ ์ Intersection Observer API๋ฅผ ์ฌ์ฉํด๋ณธ ๊ฒฝํ์ด ์์๊ธฐ ๋๋ฌธ์ ๊ณง๋ฐ๋ก Intersection Observer API๋ฅผ ์ด์ฉํด์ ๊ตฌํํด ๋ณด์๋ค.
๊ธฐ๋ณธ์ ์ธ ๋ก์ง์ observer๊ฐ ๊ด์ฐฐํด์ผ ํ target์ ๋ง๋ค๊ณ , observer๊ฐ ๊ฐ์งํ ์์ญ์ ๋ํ ์ ๋ณด๋ฅผ ๋ด์ option๊ณผ ๊ฐ์ง๋์์ ๋ API ํธ์ถ์ ํด์ค callback์ ์ ๋ฌํด ๊ตฌํํ๋ ๋ฐฉ์์ด๋ค. callbackํจ์๋ useFetch hook์ ์ฐ๊ฒฐํด๋์๋ page๋ฅผ ์ฆ๊ฐ์ํค๋๋ฐ, page ์ํ๋ฅผ ์์ useFetch์ useEffect hook์ dependency๋ก ์ ๋ฌํด๋์๊ธฐ ๋๋ฌธ์ ํ์ด์ง ๋ณํ์ ๋ฐ๋ผ api ํธ์ถ์ด ์๋์ผ๋ก ์ฐ๊ฒฐ๋๋ค.
Custom Hook: useObservation
Observation์ ํ๋ ๋ก์ง์ ๊ด์ฌ์ฌ ๋ถ๋ฆฌ๋ฅผ ํ ์ ์๊ฒ useObservation์ด๋ผ๋ Hook์ผ๋ก ๋ก์ง๋ค์ ์ ๋ฆฌํ๋ค. Hook์ ๊ด์ฐฐํ ref๋ฅผ ๋ฐํํด ref๋ฅผ ์ฐ๋ฆฌ๊ฐ ์ํ๋ ํ๊ฒ์ผ๋ก ์ฐ๊ฒฐํ ์ ์๋ค.
import { useCallback, useEffect, useRef } from "react"
const option = {
  root: null,
  rootMargin: "0px",
  threshold: 1,
}
const useObservation = onIntersect => {
  const ref = useRef(null)
  const callback = useCallback(
    (entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) onIntersect(entry, observer)
      })
    },
    [onIntersect]
  )
  useEffect(() => {
    if (!ref.current) {
      return
    }
    const observer = new IntersectionObserver(callback, option)
    observer.observe(ref.current)
    return () => observer.disconnect()
  }, [ref.current, callback])
  return ref
}
export default useObservation์ด๋ ค์ ๋ ์ ์ ์ด๊ธฐ ๋ ๋๋ง ์์ ์ ๋ฌํด์ฃผ์๋ callback์ด ์คํ๋์ด์ ๊ณ์ํด์ page 2์ธ ์ํ๋ก ์์๋๋ ์ ์ด์๋ค. ์ด๊ฒ์ ๋ง๊ธฐ ์ํด์ useFetch์ isLoading์ํ๋ฅผ ์ด์ฉํด์ ๋ก๋ฉ์ด ์๋ ๋๋ง useObservation์ ์ ๋ฌํด์ค callback ํจ์๊ฐ ์คํ๋๊ฒ ํ๋ค.
import React, { useContext } from "react"
import S from "./styles"
import IssueItem from "../issueItem/IssueItem"
import AdBox from "../adBox/AdBox"
import useFetch from "../../hooks/useFetch"
import useObservation from "../../hooks/useObservation"
import { ListContext } from "../../context/ListContext"
import Loader from "../loader/Loader"
const IssueList = () => {
  const { setNextPage } = useContext(ListContext)
  const [isLoading, error, issues] = useFetch()
  const onObserve = (entry, observer) => {
    observer.unobserve(entry.target)
    if (!isLoading) {
      setNextPage()
    }
  }
  const targetRef = useObservation(onObserve)
  return (
    <>
      <S.List>
        {Object.values(issues)
          .sort((a, b) => b.comments - a.comments)
          .map((issue, idx) => {
            if (idx === 4) {
              return (
                <div key={issue.id}>
                  <AdBox />
                  <IssueItem {...issue} />
                </div>
              )
            }
            return <IssueItem key={issue.id} {...issue} />
          })}
        {isLoading && <Loader />}
      </S.List>
      <S.Target ref={targetRef} />
    </>
  )
}
export default IssueList๋ง์ง๋ง ํ์ด์ง ๋ฌดํ API ํธ์ถ
๊ตฌํ์ด ๋ค ๋๋ ์ค ์์์ง๋ง ๋ง์ง๋ง ํ์ด์ง์์ ๊ณ์ํด์ API๊ฐ ํธ์ถ๋๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
์๋ฌ ํด๊ฒฐ์ ์ํด์ ํ์๋ถ๋ค์ ๋์์ ๋ฐ์ useFetch์ lastPage๋ผ๋ ์ํ๋ฅผ ์ถ๊ฐํ๊ณ , ๋ ์ด์ ๋ถ๋ฌ์ฌ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ๋น ๋ฐฐ์ด๋ก ๋ฐ์์ค๋ ์ ์ ์ด์ฉํด data.length๊ฐ 0์ผ ๋ lastPage๋ฅผ True๋ก ๋ฐ๊ฟ ํด๊ฒฐํ ์ ์์๋ค.
//useFetch.jsx
const useFetch = () => {
  const { issues, setIssues, page } = useContext(ListContext);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState('');
  const [lastPage, setLastPage] = useState(false);
  const getList = async () => {
    setIsLoading(true);
    try {
      const data = await getIssueList(page);
      if (data.length === 0) {
        setLastPage(true);
      }
    ...
  };
  useEffect(() => {
    getList();
  }, [page]);
  return [isLoading, error, issues, lastPage];
};
export default useFetch;
//issueList.jsx
import React, { useContext } from 'react';
import S from './styles';
import IssueItem from '../issueItem/IssueItem';
import AdBox from '../adBox/AdBox';
import useFetch from '../../hooks/useFetch';
import useObservation from '../../hooks/useObservation';
import { ListContext } from '../../context/ListContext';
import Loader from '../loader/Loader';
const IssueList = () => {
  const { setNextPage } = useContext(ListContext);
  const [isLoading, error, issues, lastPage] = useFetch();
  const onObserve = (entry, observer) => {
    observer.unobserve(entry.target);
    if (!isLoading && !lastPage) {
      setNextPage();
    }
  };
  const targetRef = useObservation(onObserve);
  return (
    <>
      <S.List>
		...
      </S.List>
      {!lastPage ? (
        <S.Target ref={targetRef} />
      ) : (
        <S.Banner>๋ง์ง๋ง ํ์ด์ง์
๋๋ค๐</S.Banner>
      )}
    </>
  );
};
๋ง์น๋ฉฐ
ํผ์ ๊ณต๋ถํ ๋๋ ๊ทธ๋ฅ ์๋ฌํด๊ฒฐ์ ์ํด์ ํด๊ฒฐ๋ฐฉ๋ฒ์ ์ฐพ๊ณ ๊ธฐ๋กํ๋ ๊ฒ ๋ค์๋๋ฐ, ํ์๋ถ๋ค์ด ๋ฌผ์ด๋ด ์ฃผ์๊ณ ๋์์ฃผ์๋ ๊ณผ์ ์ด ๋์๊ฒ ๋๋ฌด ๋๋ฌด ์์คํ ๊ฒฝํ์ด์๋ค. ๋ด๊ฐ ์ ์ด๋ ๊ฒ ์ฝ๋๋ฅผ ์งฐ๋์ง ๋ฌผ์ด ๋ด์ฃผ๋ ์ฌ๋์ด ์๋ค๋ ๊ฒ,ํผ๋๋ฐฑ๊ณผ ์ง๋ฌธ์ ํด์ฃผ์๋ ๋ถ์ด ์๋ค๋ ๊ฒ ๋ด๊ฐ ์ฑ์ฅํ ์ ์๋ ๊ธฐํ๋ผ๋ ์๊ฐ์ด ๋ค์๋ค. ์์ผ๋ก๋ ๋ ๊ณ ๋ฏผํ๊ณ ์์ ์ด๋ป๊ฒ๋ฅผ ์ ์ค๋ช ํ๋ ์ฝ๋๋ค์ ๋ด์๋ด์ ๋ ์ ์ค๋นํด ๋๊ฐ ์ ์๋ ์๊ฐ์ผ๋ก ์ผ์ ๊ฐ์ผ๊ฒ ๋ค.